package cmd

import (
	"os"
	"path"
	"path/filepath"
	"strings"

	"github.com/dave/jennifer/jen"
	"github.com/spf13/cobra"
)

// generateCmd represents the generate command
var generateCmd = &cobra.Command{
	Use:   "generate",
	Short: "Generate Go source from Flux source",
	Long: `This utility generates a Go source file that imports the Flux packages.
	All Flux packages need to also be a Go package.
	A placeholder file will be added when needed.`,
	RunE: generate,
}

var (
	pkgName,
	rootDir,
	importFile string
)

func init() {
	rootCmd.AddCommand(generateCmd)
	generateCmd.Flags().StringVar(&pkgName, "go-pkg", "", "The fully qualified Go package name of the root package.")
	generateCmd.Flags().StringVar(&rootDir, "root-dir", ".", "The root level directory for all packages.")
	generateCmd.Flags().StringVar(&importFile, "import-file", "packages.go", "Location relative to root-dir to place a file to import all generated packages.")
}

const placeholder = "placeholder.go"

func generate(cmd *cobra.Command, args []string) error {
	var goPackages []string
	err := walkDirs(rootDir, func(dir string) error {
		hasGo, hasFlux, err := dirProps(dir)
		if err != nil {
			return nil
		}
		goPath := path.Join(pkgName, dir)
		if hasFlux && !isInternal(dir) {
			// We need a package import
			goPackages = append(goPackages, goPath)
			if !hasGo {
				// We need a placeholder file to ensure this is a Go package.
				if err := savePlaceholder(filepath.Join(dir, placeholder), goPath); err != nil {
					return err
				}
			} else {
				// We no longer need the placeholder file
				os.Remove(filepath.Join(dir, placeholder))
			}
		}
		return nil
	})
	if err != nil {
		return err
	}

	// Write the import file
	f := jen.NewFile(path.Base(pkgName))
	f.HeaderComment(`// DO NOT EDIT: This file is autogenerated via the builtin command.
//
// The imports in this file ensures that all the init functions runs and registers
// the builtins for the flux runtime
`)
	f.Anon(goPackages...)
	return f.Save(filepath.Join(rootDir, importFile))
}

func walkDirs(path string, f func(dir string) error) error {
	files, err := os.ReadDir(path)
	if err != nil {
		return err
	}
	if err := f(path); err != nil {
		return err
	}

	for _, file := range files {
		if file.IsDir() {
			if err := walkDirs(filepath.Join(path, file.Name()), f); err != nil {
				return err
			}
		}
	}
	return nil
}

func dirProps(dir string) (hasGo, hasFlux bool, err error) {
	files, err := os.ReadDir(dir)
	if err != nil {
		return
	}
	for _, f := range files {
		if filepath.Ext(f.Name()) == ".go" &&
			!strings.HasSuffix(path.Base(f.Name()), "_test.go") &&
			// Do not count the placeholder file as Go because
			// otherwise we will delete it.
			f.Name() != placeholder {
			hasGo = true
		}
		if filepath.Ext(f.Name()) == ".flux" {
			hasFlux = true
		}
	}
	return
}

func isInternal(p string) bool {
	parts := strings.Split(p, "/")
	// The toplevel `internal` package is allowed
	// so start after that path element.
	for _, part := range parts[1:] {
		if part == "internal" {
			return true
		}
	}
	return false
}

func savePlaceholder(fpath, goPkgPath string) error {
	// Write the import file
	f := jen.NewFile(path.Base(goPkgPath))
	f.HeaderComment(`// DO NOT EDIT: This file is autogenerated via the builtin command.
//
// This file ensures that this directory is a Go package
`)
	return f.Save(fpath)
}
